# ActsAsTracked
# Copyright (c) 2007 Marjun Pagalan

module June
  module Acts
    # This plugin will track your AR model [changes , deletion] to be save in track table.
    # This also assumes that there is already track table.
    #
    #   class Topic < ActiveRecord::Base
    #     acts_as_tracked
    #   end
    #
    # See <tt>June::Acts::Tracked::ClassMethods#acts_as_tracked</tt>
    # for configuration options
    #
    module Tracked
      # place acts_as_tracked plugin specific errors
      class TrackedErrorsHere < RuntimeError; end

      CALLBACKS = [:clear_changed_attributes, :track_create, :track_update, :track_destroy, :set_new_version]

      # we add methods from the ClassMethods module
      def self.included(base) #:nodoc:
        base.extend(ClassMethods)
      end

      # this will act as a polymorphic link to its arbitrary object
      module ClassMethods

        # == Configuration options
        #
        # * <tt>except</tt> - Excludes fields from being saved in the tracked log.
        #
        #   class Topic < ActiveRecord::Base
        #     acts_as_tracked :except => [:title]
        #   end
        def acts_as_tracked(options = {})
          # we will not allow multiple calls, exit if already called
          return if self.included_modules.include?(June::Acts::Tracked::InstanceMethods)

          # we will initialize all needed configuration options
          except = [self.primary_key, inheritance_column, 'version', 'created_at', 'updated_at']
          except |= [options[:except]].flatten.collect(&:to_s) if options[:except]

          class_inheritable_accessor :non_tracked_attributes #we used *_attributes suffix to give clarity instead of *_columns
          self.non_tracked_attributes = except

          class_inheritable_accessor :track_table
          self.track_table = options[:track_table] || Track

          class_inheritable_accessor :backup_table
          self.backup_table = options[:backup_table] || DeletedTrackable

          # This is more for the future if we want to have more functions
          include June::Acts::Tracked::InstanceMethods
          extend  June::Acts::Tracked::SingletonMethods

          # for our trackable object to have a association to our track objects
          has_many :tracks, :as => :trackable, :order => 'tracks.version ASC' , :dependent => :destroy

          # method for diff callbacks on our object
          before_save  :set_new_version
          after_create :track_create
          after_update :track_update
          after_destroy :track_destroy
          after_save :clear_changed_attributes

          # hooks for method chaining, for tracking changes on the object and also on destroy of object
          #class_eval do
            alias_method_chain :write_attribute, :track_attribute

          #end
        end
      end

      # all class level methods
      # methods that behaves troughout the class
      module SingletonMethods

        # gets all modified objects
        def find_modified
        end

        # gets all deleted object
        def find_deleted
        end

        # restore our track back
        def restore_from_xml(xml, params = {})
          doc = REXML::Document.new(xml)
          root = doc.elements[1]
          # we call this method for recursive iteration into the elements
          loop_elements(root , params )

          # we will create new intance of this class model given,
          # from the recontruction of our object attributes from the given serialized xml
          new(params)
        end

        # returns a hash of the xml
        def loop_elements(root , params )
          root.elements.each do |el|
            if el.has_elements?
              params[el.name.to_sym] = ActiveRecord::Base.const_get(el.name.classify).new(loop_elements( el , params = {} ))
            else
              params[el.name.to_sym] = el.text
            end
          end
          params
        end

        # Executes the block with the auditing callbacks disabled.
        #
        #   Foo.without_auditing do
        #     @foo.save
        #   end
        #
        def without_tracking(&block)
          tracking_was_enabled = tracking_enabled
          disable_tracking
          returning(block.call) { enable_tracking if tracking_was_enabled }
        end

        def disable_tracking
          class_eval do
            CALLBACKS.each do |attr_name|
              alias_method attr_name, :empty_callback
            end
          end
          write_inheritable_attribute :tracking_enabled, false
        end

        def enable_tracking
          class_eval do
            CALLBACKS.each do |attr_name|
              alias_method attr_name, "orig_#{attr_name}".to_sym
            end
          end
          write_inheritable_attribute :tracking_enabled, true
        end
      end

      # all instance level methods
      # methods that behave differently on its object instance
      module InstanceMethods

        # method for knowing the current model has changed.
        def changed?(attr_name = nil)
          @tracked_attributes ||= {}
          attr_name ? @tracked_attributes.include?(attr_name.to_s) : !@tracked_attributes.empty?
        end

        # -------------------------------------
        # start of protected methods
        protected

        def set_new_version
          self.version =  next_version if new_record?
        end

        # now
        def track_create
          write_track( { :only => @tracked_attributes.keys }, :create )
        end

        # will add a row to our track table if the object being tracked is changed
        def track_update
          write_track( { :only => @tracked_attributes.keys }, :update ) if changed?
        end

        # will add a row to our track table
        def track_destroy
          # get all attributtes ot this object
          tracked_attr = self.attribute_names - non_tracked_attributes
          # will add row to our track table indicating that this object is deleted
          # TODO: there is something wrong in here, it doesnt create a track record indicating that the
          # trackable object is deleted.. for audit rail purposes
          write_track( {:only => tracked_attr }, :destroy )

          # we will get all dependent associated object
          dependents = []
          self.class.reflect_on_all_associations.each do |association|
            dependents << association.name if association.options[:dependent] == :destroy
          end

          # create an new row to our backup_table for future restore of deleted trackable objects
          write_backup( {:include => dependents } )
        end

        # clears current tracked attributes.  Called after save.
        def clear_changed_attributes
          @tracked_attributes = {}
        end

        # override write_attribute to save changes to tracked attributes
        def write_attribute_with_track_attribute(attr_name, attr_value)
          attr_name = attr_name.to_s

          # we just want to get he old attr_value and record it to tracked_attributes
          unless self.non_tracked_attributes.include?(attr_name)
            @tracked_attributes ||= {}
            # get original value
            # lets keep the old value, we could use it in the future as the plugin iterates/evolve.
            old_value = self[attr_name]
            @tracked_attributes[attr_name] = old_value
          end
          # now we proceed to the write_attribute method
          write_attribute_without_track_attribute(attr_name, attr_value)
        end



        #--------------------------------------
        # start of private methods
        private

        # accepts the object to be written to out deleted_trackables table
        # and options for the serialization of the object
        def write_backup( options = {} )
            options[:dasherize]  ||= false
            options[:skip_types] ||= true

            attrs = { :trackable_type => self.class.to_s,
                      :trackable_id => self.id,
                      :record_data => self.to_xml( options)}

            backup_table.create( attrs )
        end

        # accepts the object to be written to out track table
        # and options for the serialization of the object
        def write_track( options = {}, action = nil )
            options[:dasherize]  ||= false
            options[:skip_types] ||= true

            attrs = { :trackable_type => self.class.to_s,
                      :trackable_id => self.id,
                      :record_data => self.to_xml( options),
                      :version => next_version }
            attrs[:action] = action if action
            self.tracks.create( attrs )
        end

        # Gets the next available version for the current record, or 1 for a new record
        def next_version
          return 1 if new_record?
          (self.tracks.calculate(:max, :version) || 0) + 1
        end

      end
    end
  end
end

ActiveRecord::Base.send(:include, June::Acts::Tracked)